﻿using SdmxMl.Common;
using SdmxMl.Manager;
using SdmxMl.SmsImportCfg;
using SdmxMl.Structure;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Text;

namespace ChopPlugin
{

    /// <summary>
    /// Represents a modification demand for new version
    /// </summary>
    public class MutationVersion
    {
        #region Constants

        // to distinguish code of first levels that can be identique to secondary level code
        public const char MARKER_CODE_LEVEL_ONE = 'C';
        public const char MARKER_CODE_SUB_LEVELS = 'Z';

        // Excel source file columns index to use
        const int indexColumnAntrag = 1;
        const int indexColumnModifType = 4;
        const int indexColumnParentCode = 5;
        const int indexColumnZCode = 6;
        const int indexColumnType = 7;
        const int indexColumnTextDe = 8;
        const int indexColumnTextDeNew = 9;
        const int indexColumnLateral = 10;

        const int CODE_MAX_LENGTH = 6;

        const string MSG_ERR_CODE_CHANGE = " Kreationen und Löschungen von Codes (Titeln) werden nicht unterstützt. Bitte entfernen sie die Kreationen und Löschungen von Codes und starten den Import dieser Änderungsdatei erneut. ";
        const string MSG_ERR_LANG_TARGET = " Mit diesem Update können ausschliesslich die Sprachen FR und IT  verarbeitet werden. Bitte stellen Sie sicher, dass die Spalte ‚toUpdateLanguage’ entweder ‚FR’ oder ‚IT’ enthält und starten den Import erneut.";
        public const string MSG_ERR_ANTRAG = "Antrag: ";
        const string MSG_UNKNOWN_MODIF_TYPE = " Unknown modification type: ";
        const string MSG_INVALID_CODE = " Invalid code: ";
        const string MSG_INVALID_ZCODE = " Invalid zcode: ";
        const string MSG_INVALID_CODE_NAME = " Code Name string contains new line";

        const string CODE_MODIF_MUTATE = "M";
        const string CODE_MODIF_SUPPRESS = "S";
        const string CODE_MODIF_CREATE = "C";
        const string CODE_MODIF_I = "I";
        const string CODE_MODIF_X = "X";

        public const string CODE_TARGET = "T";

        #endregion

        #region Properties

        /// <summary>Informative antrag Nr </summary>
        public string Antrag { get; private set; }
        /// <summary>Modification type </summary>
        public ModificationType Modif { get; private set; }
        /// <summary>Item type </summary>
        public string ItemType { get; private set; }

        /// <summary>Code expressed as in documentation with prepended marker and dot separators </summary>
        public string ZCode { get; private set; }
        /// <summary>Code expressed as in codelist </summary>
        public string code { get; private set; }
        /// <summary>Parent Code expressed as in documentation with prepended marker and dot separators (in case of code creation) or empty </summary>
        public string ParentZCode { get; private set; }
        /// <summary>Parent Code expressed as in codelist </summary>
        public string parentCode { get; private set; }
        /// <summary>Targetted annotation type. If null or empty, the target ids the code itself </summary>
        public string AnnotationType { get; private set; }

        /// <summary>the original text DE </summary>
        public string TextOrg { get; private set; }

        /// <summary>The new text DE </summary>
        public string TextNew { get; private set; }

        /// <summary>The new text DE </summary>
        public bool IsLateral { get; private set; }
        #endregion

        #region Constructor

        /// <summary> C'tor for a mutation demand </summary>
        /// <param name="wh"> helper bind to datatable representing excel mutation sheet</param>
        /// <param name="row"> row index of the demand in table</param>
        public MutationVersion(WorkSheetHelper wh, int row)
        {
            // Fetch antrag id information
            Antrag = wh.GetCellContent(row, indexColumnAntrag);

            // Get type of requested modification
            string mutType = wh.GetCellContent(row, indexColumnModifType);
            switch (mutType)
            {
                case CODE_MODIF_MUTATE: Modif = ModificationType.Mutation; break;
                case CODE_MODIF_SUPPRESS: Modif = ModificationType.Suppression; break;
                case CODE_MODIF_CREATE: Modif = ModificationType.Creation; break;
                case CODE_MODIF_I: Modif = ModificationType.Ignore; break;
                case CODE_MODIF_X: Modif = ModificationType.Ignore; break;
                default: throw new ApplicationException(MSG_ERR_ANTRAG + Antrag + MSG_UNKNOWN_MODIF_TYPE + mutType);
            }

            // Code to operate on (given as CHOP ZCode)
            ZCode = wh.GetCellContent(row, indexColumnZCode);
            if ((this.code = GetCodeId(ZCode)) == null)
                throw new ApplicationException(MSG_ERR_ANTRAG + Antrag + MSG_INVALID_ZCODE + ZCode);

            // Parent code (can be null)
            ParentZCode = wh.GetCellContent(row, indexColumnParentCode);
            parentCode = GetCodeId(ParentZCode);
            if (string.IsNullOrEmpty(ParentZCode) == false && parentCode == null)
                throw new ApplicationException(MSG_ERR_ANTRAG + Antrag + MSG_INVALID_CODE + ParentZCode);

            // Type of element to operate on (an annotation type or the code name)
            ItemType = wh.GetCellContent(row, indexColumnType);

            // Text in the 3 supported languages
            TextOrg = wh.GetCellContent(row, indexColumnTextDe);
            TextNew = wh.GetCellContent(row, indexColumnTextDeNew).Replace("\n", " ");
            // For code name, newline is not allowed
            if (ItemType == CODE_TARGET)
            {
                // Laterality
                IsLateral = wh.GetCellContent(row, indexColumnLateral).Length > 1;

                if (TextNew.IndexOf('\r') >= 0 || TextNew.IndexOf('\n') >= 0)
                    throw new ApplicationException(MSG_ERR_ANTRAG + Antrag + MSG_INVALID_CODE_NAME);
            }
            else
            {
                try
                {// Annotation types
                    AnnotationType = ChopCodes.CreateAnnotationFromType(ItemType).Type;
                }
                catch (ApplicationException ex)
                {
                    throw new ApplicationException(MSG_ERR_ANTRAG + Antrag + " " + ex.Message);
                }
            }
        }

        /// <summary>
        /// String representation used for duplicate mutation detection
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.Append(ZCode).Append('.').Append(this.ItemType).Append('.').Append(this.TextOrg);
            return sb.ToString();  
        }


        #endregion

        #region ZCode

        /// <summary>Helper, extracts SDMX code from CHOP ZCode </summary>
        /// <param name="zCode">CHOP zcode notation</param>
        /// <returns>code id as presented in SDMX</returns>
        public static string GetCodeId(string zCode)
        {
            if (zCode.Length < 2 || (zCode[0] != MARKER_CODE_LEVEL_ONE && zCode[0] != MARKER_CODE_SUB_LEVELS))
                return null;
            string code = zCode.Substring(1).Replace(".", string.Empty);

            return (code.Length > CODE_MAX_LENGTH || code.Length < 1) ? null : code;
        }

        public static int GetCodelistIndex(string zCode)
        {
            string code = GetCodeId(zCode);
            if (code == null)
                throw new ApplicationException("Invalid code: " + zCode);
            return zCode[0] == MARKER_CODE_LEVEL_ONE ? 0 : code.Length - 1;
        }

        #endregion

    }

    public class ModificationVersionList
    {
        #region Internal Members

        /// <summary>Container of requested modifications </summary>
        private List<MutationVersion> mutationListFirst;
        private List<MutationVersion> mutationListSecond;
        private List<MutationVersion> mutationListThird;

        /// <summary>
        /// Key = code + Item type + libelle
        /// Value = Antrag
        /// Usage: checking duplicate mutation on same code
        /// </summary>
        private Dictionary<string, string> dicoCheckDuplicate;
        #endregion

        #region Constants

        /// <summary>Expected sheet name holding modification list</summary>
        const string MSG_PATH_MISSING = "Please provides path. for Excel 2003 CHOP modification list";
        const string MSG_COLUMN_COUNT = "Invalid CHOP mutation list, column count is too small";
        const string MSG_SHEET_NAME = "CHOP modifications must be placed in an Excel sheet named: ";
        public const string MSG_CANNOT_LOCATE_CODE = "Unable to locate SDMX code: ";
        public const string MSG_CODE_NAME_NO_MATCH = "Unexpected code text does not match for code: ";
        public const string MSG_ANNOT_NOT_FETCH = "Unable to fetch annotation ";
        const string MSG_CL_FINALIZED = "Cannot operate on finalized codelist: ";
        public const string MSG_ANTRAG = " Antrag: ";
        public const string MSG_FOR_CODE = " for code: ";


        const int ColumnCount = 8;

        #endregion

        #region Constructor

        /// <summary>C'tor</summary>
        /// <param name="path">path to Excel 2003 Chop modification file</param>
        public ModificationVersionList(string path)
        {
            dicoCheckDuplicate = new Dictionary<string, string>();

            mutationListFirst = new List<MutationVersion>();
            mutationListSecond = new List<MutationVersion>();
            mutationListThird = new List<MutationVersion>();

            Parse(LoadModifList(path));
        }

        /// <summary>Load Excel file content as a datatable</summary>
        /// <param name="path">path to Excel 2003 Chop modification file</param>
        /// <returns>a DataTable holding non checked modifications</returns>
        private DataTable LoadModifList(string path)
        {
            ExcelDataReader.ExcelDataReader spreadsheet = null;
            if (path == null)
                throw new ApplicationException(MSG_PATH_MISSING);

            FileStream fs = null;
            try
            {
                fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
                spreadsheet = new ExcelDataReader.ExcelDataReader(fs);
            }
            finally
            {
                if (fs != null)
                    fs.Close();
            }
            if (spreadsheet != null)
            {
                if (spreadsheet.WorkbookData.Tables.Count == 0)
                    throw new ApplicationException("No excel sheet found!");
            }

            return spreadsheet.WorkbookData.Tables[0];
        }

        /// <summary>
        /// Scan requested modifications and check syntax validity
        /// </summary>
        /// <param name="dt">Table holding non checked modifications requests</param>
        private void Parse(DataTable dt)
        {
            WorkSheetHelper wh = new WorkSheetHelper(dt);

            // Ensure eenough columns
            if (dt.Columns.Count < ColumnCount)
                throw new ApplicationException(MSG_COLUMN_COUNT);

            for (int row = 2; row <= dt.Rows.Count; row++)
            {
                MutationVersion mut = new MutationVersion(wh, row);
                if (mut.Modif == ModificationType.Ignore)
                    continue;

                // Check duplicate modification orders on item (conflict)
                string mutKey = mut.ToString();
                if (dicoCheckDuplicate.ContainsKey(mutKey))
                {
                    throw new ApplicationException(string.Format("Error duplicate modification between Antrag {0} and {1}.", mut.Antrag, dicoCheckDuplicate[mutKey]));
                }
                else
                {
                    dicoCheckDuplicate.Add(mutKey, mut.Antrag);
                }

                // determines priority
                if (mut.ItemType == MutationVersion.CODE_TARGET && (mut.Modif == ModificationType.Creation || mut.Modif == ModificationType.Suppression))
                {
                    mutationListFirst.Add(mut);
                }
                else if (mut.Modif == ModificationType.Suppression)
                {
                    mutationListSecond.Add(mut);
                }
                else
                    mutationListThird.Add(mut);
            }
        }
        #endregion

        #region Apply modifications

        public void Apply(SmManager sm, HierarchicalCodelistType hcl)
        {
            MutationVersionApplier mutationApplier = new MutationVersionApplier(sm, hcl);
            mutationApplier.ApplyLevelOne(mutationListFirst);
            mutationApplier.ApplyLevelTwo(mutationListSecond);
            mutationApplier.ApplyLevelThree(mutationListThird);

            // Now sort all codelists and hierarchy
            mutationApplier.SortResult();
        }

        #endregion

        #region Helper

        /// <summary> Helper for location of targeted annotation </summary>
        /// <param name="allAnnot"> item full annotation list</param>
        /// <param name="mut">requested modification</param>
        /// <returns>loacated annotation or null if not found </returns>
        private Annotation LocateTargetAnnotation(AnnotationList allAnnot, Mutation mut)
        {
            // All annotations of required type
            AnnotationList anl = allAnnot.GetAllAnnotationOfType(mut.AnnotationType, string.Empty);

            foreach (Annotation a in anl)
                if (a.Text.GetForLanguage(mut.CompareLangId) == mut.Text.GetForLanguage(mut.CompareLangId))
                    return a;
            return null;
        }
        #endregion

    }


    public class MutationVersionApplier
    {
        private HierarchicalCodelistType hcl;
        private HierarchyType hierarchy;
        private CodeListType[] arrClt;
        private List<string> removedCodes;
        private AnnotationList removedAnnotations;

        public MutationVersionApplier(SmManager sm, HierarchicalCodelistType hclTarget)
        {
            hcl = hclTarget;

            //Fetch all referred codelists
            arrClt = new CodeListType[hcl.CodelistRefList.Count];
            for (int i = 0; i < hcl.CodelistRefList.Count; i++)
                arrClt[i] = sm.GetArtefact(SdmxArtefactType.CodeLists, hcl.CodelistRefList[i]) as CodeListType;

            hierarchy = hcl.HierarchyList[0];

        }

        /// <summary>
        /// Level One means code suppression and code creation
        /// </summary>
        /// <param name="mutationList">list of mutations of type One</param>
        public void ApplyLevelOne(List<MutationVersion> mutationList)
        {
            this.removedCodes = new List<string>();
            List<MutationVersion> creationList = new List<MutationVersion>();
            foreach (MutationVersion mut in mutationList)
            {
                if (this.removedCodes.Contains(mut.code))
                    continue;

                // Code suppression
                if (mut.Modif == ModificationType.Suppression)
                {
                    SuppressCodeInHierarchy(mut);
                }
                else if (mut.Modif == ModificationType.Creation)
                {
                    creationList.Add(mut);
                }
            }
            // Code creation sort by level order
            creationList.Sort(delegate(MutationVersion p1, MutationVersion p2)
              { return p1.ZCode.CompareTo(p2.ZCode); });

            // Now apply creation
            foreach (MutationVersion mut in creationList)
            {
                // Ensure parent given and exists
                if (string.IsNullOrEmpty(mut.ParentZCode) && mut.ZCode[0] != MutationVersion.MARKER_CODE_LEVEL_ONE)
                    throw new ApplicationException(MutationVersion.MSG_ERR_ANTRAG + mut.Antrag + " Invalid create code instruction, No parent defined");

                // Create code
                CodeType c = new CodeType();
                c.Id = mut.code;
                if (string.IsNullOrEmpty(mut.TextNew))
                    throw new ApplicationException(string.Format("Antrag: {0}. Code to create: {1} has no text defined ", mut.Antrag, mut.code));

                c.Name.UpdateForLanguage("de", mut.TextNew);
                // Do we need to express laterality
                if (mut.IsLateral)
                {
                    c.AnnotationList.Add(new Annotation(ChopCodes.ANNOT_TYPE_LATERAL));
                }

                // Check not exist in hierarchy or codelist
                CodeRefType crt = this.hierarchy.LocateCodeRef(hierarchy.CodeRefList, GetAliasOfCode(mut.ZCode), mut.code);
                if (crt != null)
                    throw new ApplicationException(string.Format("Antrag: {0}. Code to create: {1} already exists in hierarchy. ", mut.Antrag, mut.code));

                CodeListType clt = this.arrClt[MutationVersion.GetCodelistIndex(mut.ZCode)];
                if (clt.CodeList.LocateItem(mut.code) != null)
                    throw new ApplicationException(string.Format("Antrag: {0}. Code to create: {1} already exists in codelist. ", mut.Antrag, mut.code));

                // Add in corresponding codelist (deduced from code length)
                clt.CodeList.Add(c);

                // Locate parent in hierarchy
                CodeRefTypeList crtList = hierarchy.CodeRefList; // assume top
                if (mut.parentCode != null)
                {
                    crt = this.hierarchy.LocateCodeRef(hierarchy.CodeRefList, GetAliasOfCode(mut.ParentZCode), mut.parentCode);
                    if (crt == null)
                        throw new ApplicationException(string.Format("Antrag: {0}. Unable to find parent code {1} in hierarchy. ", mut.Antrag, mut.parentCode));
                    crtList = crt.CodeRefList;
                }

                // Add reference to code and sort
                CodeRefType crtNew = new CodeRefType();
                crtNew.CodeID = c.Id;
                crtNew.CodelistAliasRef = GetAliasOfCode(mut.ZCode);
                crtList.Add(crtNew);
            }
        }

        public void ApplyLevelTwo(List<MutationVersion> mutationList)
        {
            removedAnnotations = new AnnotationList();
            // Now apply delete of annotations
            foreach (MutationVersion mut in mutationList)
            {
                if (this.removedCodes.Contains(mut.code))
                    continue;
                // Locate code for applying mutation
                ISdmxHierarchyListItem item = arrClt[MutationVersion.GetCodelistIndex(mut.ZCode)].CodeList.LocateItem(mut.code);
                if (item == null)
                    throw new ApplicationException(MutationVersion.MSG_ERR_ANTRAG + mut.Antrag + " " + ModificationVersionList.MSG_CANNOT_LOCATE_CODE + mut.code);
                Annotation a = LocateTargetAnnotation(item.AnnotationList, mut);
                if (a == null)
                {
                    throw new ApplicationException(ModificationVersionList.MSG_ANNOT_NOT_FETCH + mut.AnnotationType + ModificationVersionList.MSG_FOR_CODE
                        + mut.code + ModificationVersionList.MSG_ANTRAG + mut.Antrag);
                }
                else
                {
                    item.AnnotationList.Remove(a);
                    removedAnnotations.Add(a);
                }
            }
        }

        public void ApplyLevelThree(List<MutationVersion> mutationList)
        {
            foreach (MutationVersion mut in mutationList)
            {
                // Locate code for applying mutation
                ISdmxHierarchyListItem item = arrClt[MutationVersion.GetCodelistIndex(mut.ZCode)].CodeList.LocateItem(mut.code);
                if (item == null)
                    throw new ApplicationException(MutationVersion.MSG_ERR_ANTRAG + mut.Antrag + " " + ModificationVersionList.MSG_CANNOT_LOCATE_CODE + mut.code);

                // Code texte & Laterality update
                if (mut.ItemType == MutationVersion.CODE_TARGET)
                {
                    if (item.Name.GetForLanguage(LangSupport.GERMAN_ID) != mut.TextOrg)
                        throw new ApplicationException(ModificationVersionList.MSG_CODE_NAME_NO_MATCH + mut.code + ModificationVersionList.MSG_ANTRAG + mut.Antrag);
                    item.Name.UpdateForLanguage(LangSupport.GERMAN_ID, mut.TextNew);

                    // Do we need to changelaterality
                    Annotation anLateral = item.AnnotationList.GetAnnotationType(ChopCodes.ANNOT_TYPE_LATERAL);
                    if (mut.IsLateral)
                    {
                        if (anLateral == null)
                            item.AnnotationList.Add(new Annotation(ChopCodes.ANNOT_TYPE_LATERAL));
                    }
                    else if (anLateral != null)
                    {
                        item.AnnotationList.Remove(anLateral);
                    }
                }
                // Annotation Creation
                else if (mut.Modif == ModificationType.Creation)
                {
                    // Create a new annotation
                    Annotation a = new Annotation(mut.AnnotationType);
                    a.Text.UpdateForLanguage(LangSupport.GERMAN_ID, mut.TextNew);
                    item.AnnotationList.Add(a);
                }
                else // Annotation update
                {
                    // Mutation part of removed annotations are to ignore otherwise treat
                    if (LocateTargetAnnotation(removedAnnotations, mut) == null)
                    {

                        Annotation a = LocateTargetAnnotation(item.AnnotationList, mut);
                        if (a == null)
                            throw new ApplicationException(ModificationVersionList.MSG_ANNOT_NOT_FETCH + mut.AnnotationType + ModificationVersionList.MSG_FOR_CODE
                            + mut.code + ModificationVersionList.MSG_ANTRAG + mut.Antrag);
                        if (mut.Modif == ModificationType.Mutation)
                            a.Text.UpdateForLanguage(LangSupport.GERMAN_ID, mut.TextNew);
                    }
                }
            }
        }


        private void SuppressCodeInHierarchy(MutationVersion mut)
        {
            // Locate code in hierarchy
            CodeRefType crt = this.hierarchy.LocateCodeRef(hierarchy.CodeRefList, GetAliasOfCode(mut.ZCode), mut.code);
            if (crt == null)
                throw new ApplicationException(string.Format("Antrag: {0}. Unable to find code {1} in hierarchy. ", mut.Antrag, mut.parentCode));

            // remove current and all childs codes in corresponding codlists
            SuppressCode(crt);
            this.hierarchy.CodeRefList.DeleteRefcode(crt);
        }

        private void SuppressCode(CodeRefType crt)
        {
            SuppressCodeInCodelist(crt);
            foreach (CodeRefType crtChild in crt.CodeRefList)
                SuppressCode(crtChild);
        }

        private void SuppressCodeInCodelist(CodeRefType crt)
        {
            CodeListType clt = GetCodelist(crt);
            ISdmxHierarchyListItem item = null;
            if (clt == null)
                throw new ApplicationException("Unable to locate codelist for code alias: " + crt.CodelistAliasRef);
            if ((item = clt.CodeList.LocateItem(crt.CodeID)) == null)
                throw new ApplicationException(string.Format("Unable to locate code {0} from codelist {1}", crt.CodeID, clt.FullIdent));
            clt.CodeList.Remove(item);
            this.removedCodes.Add(item.Id);

        }

        private string GetAliasOfCode(string zCode)
        {
            // Codelists are ordered according to code size (code size already checked).
            return this.hcl.CodelistRefList[MutationVersion.GetCodelistIndex(zCode)].Alias;
        }

        private CodeListType GetCodelist(CodeRefType crt)
        {
            for (int index = 0; index < hierarchy.CodelistRefList.Count; index++)
            {
                if (hierarchy.CodelistRefList[index].Alias == crt.CodelistAliasRef)
                    return this.arrClt[index];
            }
            return null;
        }

        #region Helper

        /// <summary> Helper for location of targeted annotation </summary>
        /// <param name="allAnnot"> item full annotation list</param>
        /// <param name="mut">requested modification</param>
        /// <returns>loacated annotation or null if not found </returns>
        private Annotation LocateTargetAnnotation(AnnotationList allAnnot, MutationVersion mut)
        {
            // All annotations of required type
            AnnotationList anl = allAnnot.GetAllAnnotationOfType(mut.AnnotationType, string.Empty);

            foreach (Annotation a in anl)
                if (a.Text.GetForLanguage(LangSupport.GERMAN_ID) == mut.TextOrg)
                    return a;
            return null;
        }
        #endregion

        #region Sort Result

        public void SortResult()
        {
            // set hierarchical non final and sort
            hcl.IsFinal = false;
            hcl.HierarchyList[0].Sort(false);

            // Proceed same for all referred codelists
            for (int index = 0; index < hierarchy.CodelistRefList.Count; index++)
            {
                CodeListType clt = arrClt[index];
                clt.IsFinal = false;
                clt.CodeList.Sort(false);
            }


        }



        #endregion

    }

}
